Hybrid APP开发:模板本地化

上次文章介绍了Hybrid开发中常用到的Web和NA的通信方案:JSBridge,通过比较之后,最终推荐安卓和iOS通用的scheme协议,可以保证APP内和外都可以使用,今天开始正式介绍整个Hybrid架构内容。就像本系列「开篇语」提到的,这里的Hybrid是「狭义的Hybrid」,而不是所有的NA套个webapp就是Hybrid。

好的架构不是随波逐流,应该经得起考研

Hybrid技术体系是一套很多技术组成的完整知识架构。拿手百Hybrid方案,包括规范/约定、开发、联调、服务支撑等整个开发流程,而随着PWA之类「新」技术的产生又在考研技术架构面向未来的设计能力,所以手百Hybrid还在不断完善,注意是不断完善,不是推到重来!手百Hybrid技术整体架构如下:


接下来系列会围绕本架构图中重要的部分依次展开,全面的介绍Hybrid开发知识,本文介绍模板本地化开发方案。

所谓「模板本地化」,就是将Web页面内置到APP内,随版发布上线,然后通过云端接口实现更新,再直白一点:将H5网站的页面预先放到客户端发版,然后云端更新新版本。

这个方案好处在于:

  • 本地化模板提高页面打开速度,减少用户等待时间
  • 模板可更新,版本控制更方便可控,收敛快
  • Web页面和NA内置代码实现一套代码,减少开发成本
  • 上手成本小,开发就是实际开发Web(H5)页面,通过构建工具,生成Web页面和NA模板包不同的代码
  • 标准H5代码,迁移成本小,通过Node和构建工具,可以做到H5版本前后同构,将来还可以不改代码的前提下适配PWA

本文提到的JSBridge调起协议是统一使用hybrid://开头,只是提供思路和介绍整体Hybrid模板包架构和用到的技术,不涉及到具体代码,但是文章保证干货和诚意都满满😄。

模板本地化实现方案

本文会介绍两种模板本地化方案,分别是:依赖客户端拦截器(proxy)的方案一;完全无域名限制的方案二,两个方案的实现流程如下:

通过流程图来看,方案一比方案二多了拦截器。

  • 方案一是「可感知」的,根据本地缓存的模板包域名(可下发、可完全根据缓存的模板域名)进行过滤,如果本地有则读取本地缓存,如果本地没有则还是访问线上(无损)。

  • 方案二 是「预置型」的方案,只有发版时候规定的某些页面或者频道(插件)开通了Hybrid功能,才能使用Hybrid模板包缓存。

模板包设计方案

根据上面提到的两种方案,对应的模板包打包内容也不一样。具体包内容目录结构如下所示:

两种方案对比

方案一:拦截器 方案二:无拦截器
cookie 带域名符合http规范 需要native支持
入口开放 方便,无需发版 需要发版
NA开发成本 拦截器开发 浏览器能力对齐
开发联调成本 较低 较高
webapp改造成本 方便 不方便
安全性 域名限制 需要独立控制
打包方案 按照网站目录打包 按照插件/频道打包
扩展性 依照PWA模式扩展能力 -

方案一相对来说客户端开发量较大,而且拦截域名太多会有性能问题,但是优点也相对较多一些,具体需要自己权衡,本系列本意是扫盲和普及Hybrid开发知识,尽量全面的介绍Hybrid技术,不针对某种方案深入展开,后续讨论内容也是如此

浏览器能力对齐

模板本地化之后,客户端实际是通过file://协议访问本地模板,这样会导致跟域名相关的操作和方法需要客户端提供端能力支持,目前最重要的是一下几个:

  • http请求:如果在file协议下直接请求http[s]会引起跨域(Tips:跨域的几种情况有哪些?
  • cookie操作:域名没了,cookie怎么办?session也是cookie的实现
  • localstorage/sessionstorage:这俩是根据域名划分存储空间和读写的

这些浏览器的基本能力需要依靠客户端为页面提供端能力来补齐。浏览器根据域名做了限制的端能力,是遵守「同源策略」,所以在补齐端能力的时候,应该考虑进去「同源策略」,不能无节制的开通端能力

同源策略

考虑到同源策略,每个模板包会对应一个域名限制(系列文章中上一篇提到过JSBridge的安全鉴权问题),两种方案分别对应的同源策略解决方案是:

  • 方案一拦截器:代理的域名就是要限制的域名范围
  • 方案二无拦截器:在package.json/manifest.json中添加域名权限申请,比如domain:xxx.baidu.com

http请求

根据本系列上篇JSBridge的最佳实践结合常用的请求方式,设计http请求的接口如下:

从协议上来看,post和get并没有差异,但是hybrid的JSBridge协议设计是基于URL schema协议的,所以会有长度限制,对于简单的post操作使用这个协议就可以,但是对于post的data太大的情况下(不考虑上传文件等多媒体类POST操作,这方面可以通过后面文章提到的device API增强解决),需要提供

cookie操作

为了防止网站cookie被恶意修改,很多网站已经使用httponly的方式来操作cookie,这种情况下cookie其实是没有必要通过前端使用js操作的,而且在file://协议下发送http[s]://API请求,http协议也会带上域名和路径下的cookie,所以cookie的操作我是不建议开端能力的。但是凡事都有但是。。。cookie操作还是设计上吧:

hybrid://cookie/[get/set/delete]?name=value,expire=xxx;name2=value2,path=/

浏览器缓存

浏览器缓存的设计参考web storage的key-value存储设计,然后增加过期时间,类似memcached这类内存缓存,因为value是string类型,所以不像redis那样支持多种存储类型格式。

hybrid://cache/save?key=xxx&value=xxx&callback=xxx[&expire=xxx]
hybrid://cache/get?key=xxx&callback=xxx
hybrid://cache/delete?key=xxx&callback=xxx

构建工具

为了保证后续一套代码,在不同的构建流程打出不一样的模板包(比如H5版本和hybrid版本),构建过程不再是简单的压缩资源,而是根据打包类型对组件进行有选择性的pick,这里我们使用 FIS3,根据不同的 media 打出不一样的包。例如:

fis.media('hybrid').match(…)
fis.media('webapp').match(…)

详细用法请查看fis.baidu.com ,这里不再展开。

为什么要用pick?

我们的H5代码和Hybrid的代码都在一起,完全组件化之后,除了Hybrid的差异化的功能性组件,其他组件都是可以通用的。这些差异化的组件包括:

  • 上一章节提到的Http、cache和cookie这类跟域名相关的代码,我们分别封装了Hybrid内和H5版本,比如Http都叫Fetch,但是内部实现:hybrid是调用NA接口,H5是Fetch+ajax
  • 浏览和导航类:比如跳转/打开新页面,NA内是打开一个新的webview,H5是 location 跳转或直接A标签
  • 端能力和增强类组件:同样是大图浏览器,为了更好的交互,NA上做了增强,H5是纯web实现

这些差异性的代码都封装在不同的组件内,通过设计模式中的「接口模式」对外暴漏一样的接口和使用方法。

这种设计,可以更好的提高业务同事学习和编写程序的成本,同时统一的组件化管理方便单元测试和联调,(划重点:等本系列联调部分会再重提这种设计的好处,那时候会发现这设计太赞了!

代码pick怎么实现?

这里安利集鹄大叔的jdists(有对应的fis插件):https://github.com/zswang/jdists

通过jdists可以通过注释的方式对代码进行区域化裁剪和联调功能,比如下面代码可以通过fis3的media来触发trigger,从而在不同的条件下暴漏不同的代码区域:

/*<debug>*/
console.log(debug);
/*</debug>*/

/*<jdists trigger="hybrid">
var fetch = require('na/fetch')
</jdists>*/

/*<jdists trigger="webapp">
var fetch = require('web/fetch')
</jdists>*/

构建模板包

除了代码pick之外,构建工具还要和模板包管理平台配合,增加额外的文件生成(比如生成版本号、签名、diff包)做到完全自动化,然后包管理平台去拉取构建工具生成的zip包,上传到CDN平台,在数据库创建一条记录,关于模板包管理平台的内容,在下一篇文章详细介绍。

模板更新和容错策略

模板包设计好了之后,就需要考虑下发和更新的流程。除了考虑模板的更新,还需要考虑到容错机制,保证模板包升级不断出现问题或者某些极端情况下,服务可用。

模板更新

模板更新时机是很讲究的,不同的APP产品可以利用的时机可能不一样,目前针对手百产品合适的时机有:

  • APP冷热启动
  • 接口检测
  • 频道异步检查,当打开某个Hybrid频道后台开始更新
  • 页面主动检测
  • 推送

综合来说,不管怎样的更新时机,都逃不出两种更新的途径:

  1. 通过专门的NA更新接口上传本地版本号,server下发最新版本信息,完全有NA完成模板包更新
  2. 通过页面JS接口主动检测进行更新

第一种途径,比如冷热启动、接口和频道异步都是通过上行请求将APP内单个或者多个频道的当前模板版本号上传给server,server根据模板维护cms推送过来的最新版本挨个频道依次比较版本号,如果版本号有更新,就下发对应的最新流量包地址和校验信息。

而页面主动检测包括两种:

  1. 在页面发起请求的API接口中带有当前的版本号,如果接口判断有更新,则下发对应的更新包信息(下载地址和校验信息),然后页面js通过NA接口通知客户端更新
  2. 页面js直接调用NA接口强制进行一次单独的检测,这样客户端会带着版本号走专门的接口只进行模板版本的更新检测,然后走NA的更新流程

容错机制

当得知某个版本号存在不可修复的bug,需要紧急容错,可以通过server下发对应的command(app调起协议,也是一种schema)来指定在某个版本(模板版本、客户端版本)或者版本号范围内不调用Hybrid,而直接访问H5页面,达到快速止损。

举例说明:新闻列表是NA实现的,新闻的详情页是Hybrid的做的,所以Hybrid的上游是NA做的list,Hybrid详情页是通过NA的列表调起展现的。NA的list数据和调起协议(command)是由server下发的,如果server下发的是调起Hybrid页面,那么用户点击list打开Hybrid详情页;当Hybrid版本在某些情况下有问题(比如在某些客户端版本上有bug或者直接Hybrid某版本包就有bug),那么Server下发给NA调起详情页的Command就变成直接打开H5 webapp,而不是打开Hybrid,从而将有问题的Hybrid切换到线上H5,达到容错止损。

版本包回滚

除了server端止损之外,我们还设计了回滚机制,在包管理平台,可以将任意一个已经上线的版本包作为回滚包,然后生成新的版本号进行下发回滚。

客户端回滚

客户端在安装更新包之前,先将老的版本包进行备份,然后开始安装,如果安装过程遇见问题,比如覆盖失败,或者空间不够等各种问题,就需要删除之前的安装操作,将备份包重新解压,保证整个安装过程是完整的,而不是安装一半。

提高模板包的更新成功率

针对模板包的收敛率率,除了更新时机之外,还应该从链路优化和包体积方面进行优化。

具体策略如下所列:

  • 针对模板包做专门的通信线路和协议优化
    • 使用CDN就近存储
    • 断点续传
    • 分块下载
    • 重试策略
  • 减小包体积
    • 增量更新包设计

增量包设计

增量包的设计一般有两种方式,可以根据不同的场景和开发周期进行选择:

  1. 按文件diff进行增量下发
  2. 按二进制包进行增量下发

按文件diff进行增量

这种方式对于打包工具来说很简单,而且客户端没有开发工作量。

客户端拿到模板更新包是整体覆盖安装的,覆盖安装的意思是:遇见新的文件就直接新增,遇见重名的文件就直接覆盖安装。这就有点像前端静态资源管理,可以打md5进行增量,可以同名覆盖。

我们做法是当发版的时候,编译工具打出一个全量包,然后从版本库中找到最近的3次版本包,对文件进行遍历,找出diff,然后形成Vn-Vn+1的diff包,而这一切都是自动维护的。

我什么是最近3个版本包做diff,而不是全部?

因为如果全部随着发版越来越多,diff包会呈现指数增长,增加维护成本;而且模板收敛好了,两个之前老版本的量就很小了,没有必要为这些版本做增量包;再说全量包也不会大到哪里去嘛~

按二进制包进行增量下发

这种方式是将模板zip包,根据bsdiff差分算法打出不同的patch,然后将下发给客户端,客户端再加上上一个版本的包生成新的全量包。

两种方式各有利弊,实际应用要权衡成本和收益。

模板包安全

模板包在下发的过程中,会遇见被篡改的问题,而且包如果不完整,也会对Hybrid模板包的完整性和功能构成威胁。

首先整个模板包的更新接口和模板包zip的CDN都是使用HTTPS。另外在包安全性方面我们分别做了两个校验:

  1. 模板完整性检测
  2. 模板合法性校验

结合整个更新流程(客户端和server交互部分)来讲下:

在server升级接口会返回两个最重要的字段:md5signature

  • md5用于校验zip的完整性,保证zip是完整的,可解压的;
  • signature是签名,签名算法是结合私钥、请求参数、版本号、客户端特征值和模板包的md5等组合计算的,签名算法是保证了包的安全性;这样即使包被劫持替换,不知道签名算法也不会通过校验。

    写在最后

    知识是相通的,要学会举一反三,文章为了通俗易懂,很多技术介绍根据前端常见的场景做了类比。只要有心,涉猎范围够广,你会发现:
  1. 写过chrome扩展的对于模板本地化方案是不是似曾相识?
  2. 如果觉得和开发chrome扩展类似,能不能利用chrome的调试功能进行调试?
  3. 而模板本地化方案又跟PWA是不是可以通用?
  4. PWA解决的痛点是什么?和本文的模板本地化方案怎么快速切换?

这些问题都值得深入思考,且听后续文章慢慢道来~